package org.mookt.quantity;

import java.math.BigDecimal;
import java.math.RoundingMode;

import org.mookt.unit.Unit;

import com.google.common.base.Objects;
import com.google.common.base.Preconditions;

public class Quantity implements Quantifiable<Quantity> {

    public static Quantity of(final BigDecimal amount, final Unit unit) {
        return new Quantity(amount, unit);
    }

    public static Quantity of(final double amount, final Unit unit) {
        return new Quantity(BigDecimal.valueOf(amount), unit);
    }

    public static Quantity zero(final Unit unit) {
        return of(BigDecimal.ZERO, unit);
    }

    private final BigDecimal amount;

    private final Unit unit;

    private Quantity(final BigDecimal amount, final Unit unit) {
        this.amount = amount;
        this.unit = unit;
    }

    public BigDecimal getAmount() {
        return amount;
    }

    public Unit getUnit() {
        return unit;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Quantifiable#compareTo(java.lang.Object)
     */
    public int compareTo(final Quantity o) {
        checkSameUnit(this, o);
        return getAmount().compareTo(o.getAmount());
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Quantifiable#isZero()
     */
    public boolean isZero() {
        return getAmount().compareTo(BigDecimal.ZERO) == 0;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Quantifiable#isPositive()
     */
    public boolean isPositive() {
        return getAmount().compareTo(BigDecimal.ZERO) > 0;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Quantifiable#isNegative()
     */
    public boolean isNegative() {
        return getAmount().compareTo(BigDecimal.ZERO) < 0;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Quantifiable#isGreaterThan(java.lang.Object)
     */
    public boolean isGreaterThan(final Quantity other) {
        return compareTo(other) > 0;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Quantifiable#isSmallerThan(java.lang.Object)
     */
    public boolean isSmallerThan(final Quantity other) {
        return compareTo(other) < 0;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Quantifiable#plus(java.math.BigDecimal)
     */
    public Quantity plus(final BigDecimal amount) {
        return Quantity.of(getAmount().add(amount), getUnit());
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Quantifiable#plus(double)
     */
    public Quantity plus(final double amount) {
        return plus(BigDecimal.valueOf(amount));
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Quantifiable#plus(java.lang.Object)
     */
    public Quantity plus(final Quantity quantity) {
        checkSameUnit(this, quantity);
        return plus(quantity.getAmount());
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Quantifiable#minus(java.math.BigDecimal)
     */
    public Quantity minus(final BigDecimal amount) {
        return Quantity.of(getAmount().subtract(amount), getUnit());
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Quantifiable#minus(double)
     */
    public Quantity minus(final double amount) {
        return minus(BigDecimal.valueOf(amount));
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Quantifiable#minus(java.lang.Object)
     */
    public Quantity minus(final Quantity quantity) {
        checkSameUnit(this, quantity);
        return minus(quantity.getAmount());
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Quantifiable#opposite()
     */
    public Quantity opposite() {
        return Quantity.of(getAmount().negate(), getUnit());
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Quantifiable#times(java.math.BigDecimal)
     */
    public Quantity times(final BigDecimal amount) {
        return Quantity.of(amount.multiply(getAmount()), getUnit());
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Quantifiable#times(double)
     */
    public Quantity times(final double amount) {
        return times(BigDecimal.valueOf(amount));
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Quantifiable#dividedBy(java.math.BigDecimal)
     */
    public Quantity dividedBy(final BigDecimal amount) {
        return Quantity.of(getAmount().divide(amount), getUnit());
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return amount.toPlainString() + " " + unit.getSymbol();
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Quantifiable#round(int, java.math.RoundingMode)
     */
    public Quantity round(final int decimalPlaces,
            final RoundingMode roundingMode) {
        Preconditions
                .checkNotNull("Rounding mode cannot be null", roundingMode);
        return Quantity.of(amount.setScale(decimalPlaces, roundingMode), unit);
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Quantifiable#pow(int)
     */
    public Quantity pow(final int n) {
        return Quantity.of(amount.pow(n), unit);
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Quantifiable#dividedBy(double)
     */
    public Quantity dividedBy(final double amount) {
        return dividedBy(BigDecimal.valueOf(amount));
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(final Object obj) {
        if (obj instanceof Quantity) {
            final Quantity that = (Quantity) obj;
            return amount.compareTo(that.amount) == 0 && unit.equals(that.unit);
        } else {
            return false;
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        return Objects.hashCode(amount, unit);
    }

    private static void checkSameUnit(final Quantity q1, final Quantity q2) {
        final String NULL_QUANTITY = "Quantities cannot be null";

        Preconditions.checkNotNull(q1, NULL_QUANTITY);
        Preconditions.checkNotNull(q2, NULL_QUANTITY);
        Preconditions.checkArgument(q1.getUnit().equals(q2.getUnit()),
                "Units differ: %s/%s", q1.getUnit(), q2.getUnit());
    }
}